package com.zrx.govern.component.easyexcel.starter.service.impl;

import cn.hutool.core.util.ClassUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.github.junrar.Archive;
import com.github.junrar.exception.RarException;
import com.github.junrar.rarfile.FileHeader;
import com.zrx.govern.component.easyexcel.starter.Exception.ExcelModelOnlyOneOperationException;
import com.zrx.govern.component.easyexcel.starter.annotation.ExcelException;
import com.zrx.govern.component.easyexcel.starter.config.ExcelConfigProperties;
import com.zrx.govern.component.easyexcel.starter.constants.ExcelCheckConstants;
import com.zrx.govern.component.easyexcel.starter.constants.RedisConstants;
import com.zrx.govern.component.easyexcel.starter.entity.*;
import com.zrx.govern.component.easyexcel.starter.enums.ExcelEnum;
import com.zrx.govern.component.easyexcel.starter.enums.ExcelProcessEnum;
import com.zrx.govern.component.easyexcel.starter.enums.ImportType;
import com.zrx.govern.component.easyexcel.starter.handler.CustomRowWriteHandler;
import com.zrx.govern.component.easyexcel.starter.listener.ExcelAnalysisListener;
import com.zrx.govern.component.easyexcel.starter.service.EasyExcelService;
import com.zrx.govern.component.easyexcel.starter.strategy.HeadStyleStrategy;
import com.zrx.govern.component.easyexcel.starter.utils.*;
import com.zrx.govern.component.easyexcel.starter.validate.ExcelValidator;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author zhangxiaowei
 * @desc EasyExcel通用操作组件
 */
@Slf4j
@Component
@Validated
public class EasyExcelOperationService {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ExcelConfigProperties excelConfigProperties;

    @Autowired
    private ExcelValidator excelValidator;

    /**
     * 获取当前导入进度
     *
     * @param model json内定义的导入对象
     * @param type  1:导入 2:导出 3:校验
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    @ExcelException(model = "#model")
    public String getProgress(@NotBlank(message = "model不能为空") String model, @NotBlank(message = "type不能为空") String type) {
        ExcelProcessEnum excelProcessEnum = ExcelProcessEnum.getExcelProcessEnumByValue(type);
        if (Objects.isNull(excelProcessEnum)) {
            throw new RuntimeException("获取导入导出进度传参type非法");
        }
        //进度key
        String processKey = "";
        if (ExcelProcessEnum.IMPORT == excelProcessEnum) {
            processKey = getRedisKey(model, RedisConstants.IMPORT_PROGRESS);
        } else if (ExcelProcessEnum.EXPORT == excelProcessEnum) {
            processKey = getExportRedisKey(model, RedisConstants.EXPORT_PROGRESS);
        } else {
            processKey = getRedisKey(model, RedisConstants.CHECK_PROGRESS);
        }
        /*判断当前是否有当前导出任务的key,如果没有说明业务端还在业务处理(像sql查询阶段),返回进度0*/
        if (redisTemplate.hasKey(processKey)) {
            return (String) redisTemplate.opsForValue().get(processKey);
        } else {
            return "0";
        }
    }

    /**
     * 读取文件，执行校验逻辑
     *
     * @param importTypes
     * @param model
     * @param file
     * @return
     */
    @SneakyThrows
    private ExcelAnalysisListener getExcelAnalysisListener(List<ImportType> importTypes, String model, MultipartFile file) {
        /* 解析文件 */
        String originalFileName = file.getOriginalFilename();
        String suffix = StringUtils.substringAfterLast(originalFileName, ".");
        List<MultipartFile> multipartFiles = new ArrayList<>();
        if (StringUtils.equalsIgnoreCase("zip", suffix)) {
            multipartFiles.addAll(unzipToMultipartFileList(file));
        } else if (StringUtils.equalsIgnoreCase("rar", suffix)) {
            multipartFiles.addAll(unrarToMultipartFileList(file));
        } else {
            multipartFiles.add(file);
        }

        Map<Integer, List<FieldRule>> fieldRules = JsonParseUtil.getImportFieldRules(model);
        ExcelModel excelModel = JsonParseUtil.getExcelModel(model);
        /*校验进度*/
        String key = getRedisKey(model, RedisConstants.CHECK_PROGRESS);
        ExcelCheckImportProcess excelCheckImportProcess = null;
        RedisTemplate processRedisTemplate = null;
        if (excelConfigProperties.getProcessSwitch()) {
            int excelTotalRows = 0;
            for (MultipartFile f : multipartFiles) {
                excelTotalRows += ExcelUtil.getTotalRowsByUploadFile(f);
            }
            if (excelTotalRows == 0) {
                throw new RuntimeException("excel文件数据不允许为空");
            }
            excelCheckImportProcess = ExcelCheckImportProcess.builder().start(0).total(excelTotalRows).checkRedisKey(key).build();
            processRedisTemplate = redisTemplate;
        }
        ExcelAnalysisListener excelAnalysisListener = new ExcelAnalysisListener(fieldRules, importTypes, excelValidator,
                model, excelCheckImportProcess, processRedisTemplate, multipartFiles.size());
        for (MultipartFile f : multipartFiles) {
            EasyExcel.read(f.getInputStream(), excelAnalysisListener).autoCloseStream(true).headRowNumber(excelModel.getDataStartRowNumber())
                    .autoTrim(true).ignoreEmptyRow(true).sheet().doRead();
        }
        return excelAnalysisListener;
    }

    public static List<MultipartFile> unzipToMultipartFileList(MultipartFile file) throws IOException {
        List<MultipartFile> multipartFiles = new ArrayList<>();

        try (InputStream is = file.getInputStream();
             ArchiveInputStream ais = new ZipArchiveInputStream(is)) {

            ArchiveEntry entry;
            while ((entry = ais.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = ais.read(buffer)) != -1) {
                        baos.write(buffer, 0, len);
                    }
                    MultipartFile multipartFile = new MockMultipartFile(
                            entry.getName(),
                            entry.getName(),
                            null,
                            baos.toByteArray()
                    );
                    multipartFiles.add(multipartFile);
                }
            }
        }

        return multipartFiles;
    }

    public static List<MultipartFile> unrarToMultipartFileList(MultipartFile file) throws IOException, RarException {
        List<MultipartFile> multipartFiles = new ArrayList<>();

        try (InputStream is = file.getInputStream();
             Archive archive = new Archive(is)) {

            FileHeader fileHeader;
            while ((fileHeader = archive.nextFileHeader()) != null) {
                if (!fileHeader.isDirectory()) {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    archive.extractFile(fileHeader, baos);
                    MultipartFile multipartFile = new MockMultipartFile(
                            fileHeader.getFileName(),
                            fileHeader.getFileName(),
                            null,
                            baos.toByteArray()
                    );
                    multipartFiles.add(multipartFile);
                }
            }
        }

        return multipartFiles;
    }

    /**
     * 取消excel上传,删除导入唯一校验key(重新上传文件)
     *
     * @param model
     */
    public void cancelExcelImport(String model) {
        String taskKey = RedisConstants.EXCEL_TASK + model;
        if (!redisTemplate.hasKey(taskKey)) {
            throw new RuntimeException("导入校验任务不存在,请检查model是否有误");
        }
        redisTemplate.delete(taskKey);
        /*导入校验进度key*/
        String uploadKey = getRedisKey(model, RedisConstants.CHECK_PROGRESS);
        if (redisTemplate.hasKey(uploadKey)) {
            redisTemplate.delete(uploadKey);
        }
        /*设置取消上传导入的信号*/
        String uploadCancelKey = RedisConstants.EXCEL_UPLOAD_CANCEL + model;
        /*uploadCancelKey存在，说明还在导入*/
        if (redisTemplate.hasKey(uploadCancelKey)) {
            redisTemplate.boundValueOps(uploadCancelKey).set(true);
        }
        /*删除excel读取缓存到redis的数据*/
        clearModelRedisData(model);
    }

    /**
     * 校验任务，保证同一个model当前只允许一个人操作
     *
     * @param model
     * @return
     */
    private void checkExcelReadTask(String model) {
        String key = RedisConstants.EXCEL_TASK + model;
        if (redisTemplate.hasKey(key)) {
            throw new ExcelModelOnlyOneOperationException("当前导入任务已有人在操作,请等候...");
        }
    }

    /**
     * 校验读取excel,持久化至redis
     *
     * @param model
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    @ExcelException(model = "#model")
    @Transactional(rollbackFor = Exception.class)
    public ExcelCheckResult excelRead(@NotBlank(message = "model不能为空") String model, MultipartFile file) {
        if (Objects.isNull(file)) {
            throw new RuntimeException("file对象不能为空");
        }
        /*1.excel解析获取数据*/
        List<ImportType> importTypes = JsonParseUtil.getImportType(model);
        /*不为快速导入，校验任务，保证同一个model当前只允许一个人操作*/
        if (!importTypes.contains(ImportType.FAST_IMPORT)) {
            checkExcelReadTask(model);
        }
        /*解析excel*/
        ExcelAnalysisListener excelAnalysisListener = getExcelAnalysisListener(importTypes, model, file);
        List<JSONObject> dataList = excelAnalysisListener.getDataList();
        /*2.判断导入类型是否只为快速导入*/
        if (importTypes.size() == 1 && importTypes.contains(ImportType.FAST_IMPORT)) {
            /*调用导入接口直接将读取的数据导入*/
            fastImportStorage(model, dataList);
            return ExcelCheckResult.builder().total(dataList.size()).message("导入成功").build();
        }
        /*生成限定任务key*/
        String key = RedisConstants.EXCEL_TASK + model;
        redisTemplate.opsForValue().set(key, model, 10, TimeUnit.MINUTES);
        /*3.将excel解析的数据存入redis*/
        String dataKey = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_ALL);
        redisTemplate.opsForValue().set(dataKey, JSON.toJSONString(dataList), 10, TimeUnit.MINUTES);
        String checkPassKey = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_SUCCESS);
        redisTemplate.opsForValue().set(checkPassKey, JSON.toJSONString(excelAnalysisListener.getCheckPassList()), 10, TimeUnit.MINUTES);
        return ExcelCheckResult.builder().successNum(excelAnalysisListener.getCheckPassList().size()).total(dataList.size()).message("当前导入任务10分钟内有效,请尽快操作!").build();
    }

    /**
     * 快速导入持久化
     *
     * @param model
     * @param list
     */
    private void fastImportStorage(String model, List<JSONObject> list) {
        if (CollectionUtils.isEmpty(list)) {
            throw new RuntimeException("导入的数据不能为空");
        }
        /*1.获取excel导入具体实现的bean*/
        EasyExcelService easyExcelService = ExcelBeanUtil.getEasyExcelServiceBean(model);
        StorageBean storageBean = JsonParseUtil.getStorageBean(model);
        if (Objects.isNull(easyExcelService) && Objects.isNull(storageBean)) {
            throw new RuntimeException(String.format("json文件model为%s持久化的bean未配置,请修改", model));
        }
        /* 2.持久化，快速导入500一组效率相对较高*/
        List<List<JSONObject>> partitionList = ListUtils.partition(list, 500);
        if (Objects.nonNull(easyExcelService)) {
            partitionList.forEach(item -> {
                easyExcelService.saveBatchExcelData(item);
            });
        } else {
            Object bean = ExcelBeanUtil.getExecuteBean(storageBean.getBeanName());
            partitionList.forEach(item -> {
                saveBatchData(bean, storageBean.getSaveBatchMethod(), item);
            });
        }

    }

    /**
     * 从redis内获取excel校验通过的数据
     *
     * @param model
     * @return
     */
    private List<Map<Integer, String>> getExcelCheckData(String model) {
        /*1.从redis获取校验结果*/
        String key = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_ALL);
        String result = redisTemplate.opsForValue().get(key).toString();
        List<JSONObject> data = JSON.parseArray(result, JSONObject.class);

        /*2.将校验结果导出*/
        List<ExcelField> fieldList = getFieldListWithErrorColumn(model);

        /*组装数据*/
        List<Map<Integer, String>> excelWriteData = getResortedDataList(data, JSONObject.class, fieldList);
        return excelWriteData;
    }

    /**
     * 添加错误信息列
     *
     * @param model
     * @return
     */
    private List<ExcelField> getFieldListWithErrorColumn(String model) {
        List<ExcelField> fieldList = JsonParseUtil.getAllFields(model);
        /*新增错误信息列,用于排序*/
        fieldList.add(ExcelField.builder().index(fieldList.size()).fieldEnName(ExcelCheckConstants.ERROR_MSG)
                .fieldName(ExcelCheckConstants.ERROR_HEAD_COL).width(50).build());
        return fieldList;
    }

    /**
     * 下载校验excel数据
     *
     * @param model
     * @param response
     */
    @ExcelException(model = "#model")
    public void downloadCheckExcel(@NotBlank(message = "model不能为空") String model, HttpServletResponse response) {
        List<Map<Integer, String>> excelWriteData = getExcelCheckData(model);
        /*2.将校验结果导出*/
        List<ExcelField> fieldList = getFieldListWithErrorColumn(model);
        List<List<String>> headList = ExcelBeanUtil.getHeads(fieldList);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String fileName = ExcelCheckConstants.CHECK_RESULT + LocalDateTime.now().format(formatter) + ExcelEnum.XLSX.getValue();
        /*组装数据*/
        String template = JsonParseUtil.getTemplateName(model);
        if (StringUtils.isNotBlank(template)) {
            ExcelUtil.downloadExcelWithTemplate(excelConfigProperties, model, response, excelWriteData, fieldList, fileName, ExcelEnum.XLSX, null, true);
        } else {
            ExcelUtil.downloadExcelDynamicHead(response, excelWriteData, fileName, "sheet1", headList, ExcelEnum.XLSX, null, fieldList, null);
        }
    }

    /**
     * 下载校验excel数据
     * todo 测试用
     *
     * @param model
     */
    @ExcelException(model = "#model")
    public void downloadCheckExcelWithTemplateTest(@NotBlank(message = "model不能为空") String model, HttpServletResponse response, List<Map<Integer, String>> excelWriteData) {
        //List<Map<Integer, String>> excelWriteData = getExcelCheckData(model);

        /*2.将校验结果导出*/
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String fileName = ExcelCheckConstants.CHECK_RESULT + LocalDateTime.now().format(formatter) + ExcelEnum.XLSX.getValue();
        List<ExcelField> fieldList = JsonParseUtil.getAllFields(model);
        /*组装数据*/
        ExcelUtil.downloadExcelWithTemplate(excelConfigProperties, model, response, excelWriteData, fieldList, fileName, ExcelEnum.XLSX, null, true);
    }

    /**
     * 导出单个excel数据并计算进度
     *
     * @param model
     * @author zhangxiaowei
     **/
    public void excelDownload(@NotBlank(message = "model不能为空") String model, HttpServletResponse response, @NotNull List<? extends Object> data,
                              @NotBlank(message = "fileName不能为空") String fileName, String sheetName, @NotNull Class clazz, ExcelEnum excelEnum) {
        if (CollectionUtils.isEmpty(data)) {
            throw new RuntimeException("导出的excel数据不允许为空");
        }
        List<ExcelField> fieldList = JsonParseUtil.getAllFields(model);
        List<List<String>> headList = ExcelBeanUtil.getDynamicHead(clazz, fieldList);
        String exportRedisKey = getExportRedisKey(model, RedisConstants.EXPORT_PROGRESS);
        CustomRowWriteHandler customRowWriteHandler = excelConfigProperties.getProcessSwitch() ? buildCustomCellWriteHandler(data.size(), exportRedisKey) : null;
        /*组装数据*/
        List<Map<Integer, String>> excelWriteData = getResortedDataList(data, clazz, fieldList);
        /*判断当前导出配置的json，模板文件是否存在，存在即在模版基础上添加数据(保留原有的sheetName)*/
        /*下载excel*/
        String template = JsonParseUtil.getTemplateName(model);
        if (StringUtils.isNotBlank(template)) {
            ExcelUtil.downloadExcelWithTemplate(excelConfigProperties, model, response, excelWriteData, fieldList, fileName, ExcelEnum.XLSX, customRowWriteHandler, false);
        } else {
            ExcelUtil.downloadExcelDynamicHead(response, excelWriteData, fileName, sheetName, headList, excelEnum, customRowWriteHandler, fieldList, null);
        }
    }

    /**
     * 根据动态表头导出excel数据(非model形式)
     *
     * @param key               导出excel计算进度的redis key(不要进度可传空)
     * @param fileName
     * @param headStyleStrategy 表头自定义样式,传空将使用默认样式
     * @author zhangxiaowei
     **/
    public void excelDownloadWithDynamicHead(String key, HttpServletResponse response, @NotNull List<JSONObject> data, @NotNull List<DynamicHead> dynamicHeadList,
                                             @NotBlank(message = "fileName不能为空") String fileName, String sheetName, ExcelEnum excelEnum, HeadStyleStrategy headStyleStrategy) {
        if (CollectionUtils.isEmpty(data)) {
            throw new RuntimeException("导出的excel数据不允许为空");
        }
        if (StringUtils.isNotBlank(key) && redisTemplate.hasKey(key)) {
            throw new RuntimeException("redis key已存在,请更改");
        }
        dynamicHeadList.sort(Comparator.comparing(DynamicHead::getIndex));
        for (int i = 0; i < dynamicHeadList.size(); i++) {
            if (i != dynamicHeadList.get(i).getIndex()) {
                throw new RuntimeException("headList内index不连续，请修改");
            }
        }
        //获取排好序的ExcelField集合
        CustomRowWriteHandler customRowWriteHandler = null;
        if (StringUtils.isNotBlank(key)) {
            customRowWriteHandler = excelConfigProperties.getProcessSwitch() ? buildCustomCellWriteHandler(data.size(), key) : null;
        }
        /*生成表头并组装数据*/
        List<ExcelField> fieldList = JSONArray.parseArray(JSON.toJSONString(dynamicHeadList), ExcelField.class);
        List<List<String>> headList = ExcelBeanUtil.getHeads(fieldList);
        List<Map<Integer, String>> excelWriteData = getResortedDataList(data, JSONObject.class, fieldList);
        /*下载excel*/
        ExcelUtil.downloadExcelDynamicHead(response, excelWriteData, fileName, sheetName, headList, excelEnum, customRowWriteHandler, fieldList, headStyleStrategy);
    }

    /**
     * 导出单个excel数据并计算进度(无model 支持@ExcelProperty模式)
     *
     * @param key      导出excel计算进度的redis key(不要进度可传空)
     * @param fileName
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    public void excelDownloadWithExcelProperty(String key, HttpServletResponse response, @NotNull List<? extends Object> data,
                                               @NotBlank(message = "fileName不能为空") String fileName, String sheetName, @NotNull Class clazz, ExcelEnum excelEnum) {
        if (CollectionUtils.isEmpty(data)) {
            throw new RuntimeException("导出的excel数据不允许为空");
        }
        if (StringUtils.isNotBlank(key) && redisTemplate.hasKey(key)) {
            throw new RuntimeException("redis key已存在,请更改");
        }
        //获取排好序的ExcelField集合
        CustomRowWriteHandler customRowWriteHandler = null;
        if (StringUtils.isNotBlank(key)) {
            customRowWriteHandler = excelConfigProperties.getProcessSwitch() ? buildCustomCellWriteHandler(data.size(), key) : null;
        }
        /*下载excel*/
        ExcelUtil.downloadExcel(response, data, fileName, sheetName, clazz, excelEnum, customRowWriteHandler);
    }

    /**
     * 根据json定义导出的filed顺序,构建写入Excel的数据,使其对应
     *
     * @return
     */
    @SneakyThrows
    private List<Map<Integer, String>> getResortedDataList(List<? extends Object> data, Class clazz, List<ExcelField> fieldList) {
        /*参数类型的校验*/
        if (clazz == String.class || ClassUtil.isBasicType(clazz)) {
            throw new RuntimeException(String.format("Class:%s,不支持的数据类型", clazz.getName()));
        }
        List<Map<Integer, String>> list = new ArrayList<>(data.size());
        if (clazz == JSONObject.class) {
            JSONObject jsonObject = null;
            for (int i = 0; i < data.size(); i++) {
                jsonObject = (JSONObject) data.get(i);
                Map<Integer, String> dataMap = new HashMap<>(fieldList.size());
                JSONObject finalJsonObject = jsonObject;
                fieldList.forEach(el -> {
                    dataMap.put(el.getIndex(), finalJsonObject.getString(el.getFieldEnName()));
                });
                list.add(dataMap);
            }
        } else if (clazz == Map.class) {
            Map<String, Object> map = null;
            AtomicReference<Object> val = null;
            for (int i = 0; i < data.size(); i++) {
                map = (Map<String, Object>) data.get(i);
                Map<Integer, String> dataMap = new HashMap<>(fieldList.size());
                Map<String, Object> finalMap = map;
                fieldList.forEach(el -> {
                    val.set(finalMap.get(el.getFieldEnName()));
                    dataMap.put(el.getIndex(), Objects.nonNull(val.get()) ? val.get().toString() : null);
                });
                list.add(dataMap);
            }
        } else {
            Object obj = null;
            /*获取导出字段集合(已按json定义的结构排好序)*/
            List<ExcelExportField> excelExportFields = ExcelBeanUtil.getSortedExcelExportFields(clazz, fieldList);
            SimpleDateFormat simpleDateFormat = ExcelBeanUtil.getSimpleDateFormat(excelExportFields);
            DateTimeFormatter dateTimeFormatter = ExcelBeanUtil.getDateTimeFormatter(excelExportFields);
            Field field = null;
            Object val = null;
            ExcelExportField excelExportField = null;
            for (int i = 0; i < data.size(); i++) {
                obj = data.get(i);
                Map<Integer, String> dataMap = new HashMap<>(excelExportFields.size());
                for (int j = 0; j < excelExportFields.size(); j++) {
                    excelExportField = excelExportFields.get(j);
                    field = excelExportField.getField();
                    field.setAccessible(true);
                    val = field.get(obj);
                    if (excelExportField.getFieldClassType().equals(Date.class)) {
                        dataMap.put(j, Objects.nonNull(val) ? simpleDateFormat.format(val) : null);
                    } else if (excelExportField.getFieldClassType().equals(LocalDateTime.class)) {
                        dataMap.put(j, Objects.nonNull(val) ? ((LocalDateTime) val).format(dateTimeFormatter) : null);
                    } else if (excelExportField.getFieldClassType().equals(LocalDate.class)) {
                        dataMap.put(j, Objects.nonNull(val) ? ((LocalDate) val).format(dateTimeFormatter) : null);
                    } else {
                        dataMap.put(j, Objects.nonNull(val) ? val.toString() : null);
                    }
                }
                list.add(dataMap);
            }
        }
        return list;
    }

    /**
     * 构建进度解析器
     *
     * @param key
     * @param size
     * @return
     */
    private CustomRowWriteHandler buildCustomCellWriteHandler(Integer size, String key) {
        ExcelExportProcess excelExportProcess = ExcelExportProcess.builder().start(0).total(size).exportRedisKey(key).build();
        CustomRowWriteHandler customRowWriteHandler = new CustomRowWriteHandler(excelExportProcess, redisTemplate);
        return customRowWriteHandler;
    }

    /**
     * 生成file对象,返回文件地址
     *
     * @param model
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    public String excelWrite(@NotBlank(message = "model不能为空") String model, @NotNull List<? extends Object> data,
                             @NotBlank(message = "fileName不能为空") String fileName, String sheetName, @NotNull Class clazz, ExcelEnum excelEnum) {
        if (CollectionUtils.isEmpty(data)) {
            throw new RuntimeException("导出的excel数据不允许为空");
        }
        List<ExcelField> fieldList = JsonParseUtil.getAllFields(model);
        List<List<String>> headList = ExcelBeanUtil.getDynamicHead(clazz, fieldList);
        /*组装数据*/
        List<Map<Integer, String>> excelWriteData = getResortedDataList(data, clazz, fieldList);
        return ExcelUtil.writeExcelDynamicHead(excelConfigProperties, excelWriteData, fileName, sheetName, headList, excelEnum, null, fieldList);
    }

    /**
     * 批量生成excel文件并压缩为zip包进行下载
     * zip文件和excel文件最后会进行删除
     *
     * @param zipName
     * @param fileList
     * @param model    如果为导出多个文件,model为前端传入格式为a_b_c
     * @param response
     * @return: void
     * @author zhangxiaowei
     **/
    public void downloadZip(@NotBlank(message = "zip文件名不能为空") String zipName, @NotBlank(message = "model不能为空") String model,
                            @NotNull(message = "fileList不能为空") @Valid List<MultiExcelFile> fileList, HttpServletResponse response) {
        if (!StringUtils.contains(zipName, ExcelCheckConstants.ZIP_FILE)) {
            throw new RuntimeException("非法的zip文件名");
        }
        List<String> excelFileList = new ArrayList<>(fileList.size());
        String exportKey = getExportRedisKey(model, RedisConstants.EXPORT_PROGRESS);
        /*计算导出excel文件总行数*/
        Integer count = 0;
        for (MultiExcelFile multiExcelFile : fileList) {
            count += multiExcelFile.getData().size();
        }
        /*生成excel文件并计算进度*/
        ExcelExportProcess excelExportProcess = ExcelExportProcess.builder().start(0).total(count).exportRedisKey(exportKey).build();
        CustomRowWriteHandler customRowWriteHandler = excelConfigProperties.getProcessSwitch() ? new CustomRowWriteHandler(excelExportProcess, redisTemplate) : null;
        AtomicInteger start = new AtomicInteger();
        CustomRowWriteHandler finalCustomRowWriteHandler = customRowWriteHandler;
        fileList.forEach(multiExcelFile -> {
            List<ExcelField> fieldList = JsonParseUtil.getAllFields(multiExcelFile.getModel());
            List<List<String>> headList = ExcelBeanUtil.getDynamicHead(multiExcelFile.getClazz(), fieldList);
            /*组装数据*/
            List<Map<Integer, String>> excelWriteData = getResortedDataList(multiExcelFile.getData(), multiExcelFile.getClazz(), fieldList);
            excelFileList.add(ExcelUtil.writeExcelDynamicHead(excelConfigProperties, excelWriteData, multiExcelFile.getFileName(), multiExcelFile.getSheetName(), headList, multiExcelFile.getExcelEnum(), finalCustomRowWriteHandler, fieldList));
            /*重置计算进度的开始值,从而控制多个excel导出进度的计算*/
            start.addAndGet(multiExcelFile.getData().size());
            excelExportProcess.setStart(start.get());
        });
        FileZipUtil.downloadZipFile(excelFileList, zipName, response, excelConfigProperties);
    }

    /**
     * 持久化当前excel校验通过的数据并计算进度(可取消导入)
     *
     * @param model
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    @Transactional(rollbackFor = Exception.class)
    @ExcelException(model = "#model")
    public Integer storage(@NotBlank(message = "model不能为空") String model) {
        /*1.获取excel导入具体实现的bean或者自定义持久化的bean*/
        EasyExcelService easyExcelService = ExcelBeanUtil.getEasyExcelServiceBean(model);
        StorageBean storageBean = JsonParseUtil.getStorageBean(model);
        if (Objects.isNull(easyExcelService) && Objects.isNull(storageBean)) {
            throw new RuntimeException(String.format("json文件model为%s持久化的bean未配置,请修改", model));
        }
        /*2.判断当前excel导入的类型，如果只是快速导入,提示异常*/
        List<ImportType> importTypes = JsonParseUtil.getImportType(model);
        if (importTypes.size() == 1 && importTypes.contains(ImportType.FAST_IMPORT)) {
            log.error("快速导入不允许重复持久化");
            throw new RuntimeException("json文件importType导入类型配置异常,请修改");
        }
        /*3.从redis获取校验通过的数据*/
        String key = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_SUCCESS);
        String result = redisTemplate.opsForValue().get(key).toString();
        List<JSONObject> list = JSON.parseArray(result, JSONObject.class);
        /**
         * 根据model从json解析获取cancelKey和redisImportKey
         */
        String cancelKey = getCancelRedisKey(model);
        redisTemplate.boundValueOps(cancelKey).set(false);
        String redisImportKey = getRedisKey(model, RedisConstants.IMPORT_PROGRESS);
        /*3.数据进行分组并持久化*/
        List<List<JSONObject>> partitionList = ListUtils.partition(list, 500);
        AtomicInteger realCnt = new AtomicInteger();
        try {
            partitionList.forEach(item -> {
                /*4.判断当前是否取消导入*/
                Boolean isCancel = (Boolean) redisTemplate.boundValueOps(cancelKey).get();
                if (isCancel) {
                    log.info("导入终止...");
                    throw new RuntimeException("导入终止...");
                }
                if (Objects.nonNull(easyExcelService)) {
                    easyExcelService.saveBatchExcelData(item);
                } else {
                    /*调用反射执行批量持久化*/
                    Object bean = ExcelBeanUtil.getExecuteBean(storageBean.getBeanName());
                    saveBatchData(bean, storageBean.getSaveBatchMethod(), item);
                }
                /*5.计算当前导入进度并存入redis*/
                if (excelConfigProperties.getProcessSwitch()) {
                    BigDecimal percent = calRadio(list.size(), realCnt.addAndGet(item.size()));
                    redisTemplate.opsForValue().set(redisImportKey, String.valueOf(percent), 5, TimeUnit.SECONDS);
                }
            });
        } finally {
            /*删除任务取消key*/
            redisTemplate.delete(cancelKey);
            /*6.移除导入redis数据*/
            if (excelConfigProperties.getProcessSwitch()) {
                redisTemplate.delete(redisImportKey);
            }
            /*导入成功后，删除任务key*/
            String taskKey = RedisConstants.EXCEL_TASK + model;
            redisTemplate.delete(taskKey);
        }
        return list.size();
    }

    /**
     * 通过反射执行批量持久化方法
     *
     * @param bean
     * @param method
     * @param jsonObjects
     */
    @SneakyThrows
    private void saveBatchData(Object bean, String method, List<JSONObject> jsonObjects) {
        Class clazz = bean.getClass();
        Method saveBatchMethod = clazz.getDeclaredMethod(method, List.class);
        saveBatchMethod.setAccessible(true);
        saveBatchMethod.invoke(bean, jsonObjects);
    }

    /**
     * 获取进度控制key
     *
     * @param model
     * @return
     */
    private String getCancelRedisKey(String model) {
        return RedisConstants.EXCEL_STORAGE + model;
    }

    /**
     * 获取动态redis的key
     *
     * @param model
     * @param key
     * @return
     */
    private String getRedisKey(String model, String key) {
        return String.format(key, model);
    }

    /**
     * 获取进度控制key
     *
     * @param model
     * @return
     */
    private String getExportRedisKey(String model, String key) {
        HttpServletRequest request =
                ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        String token = request.getHeader("Authorization");
        if (StringUtils.isNotBlank(token)) {
            token = StringUtils.substringAfter(token, "Bearer ");
        } else {
            return String.format(key, model, IpUtils.getIpAddress(request));
        }
        return String.format(key, model, token + "_" + IpUtils.getIpAddress(request));
    }

    /**
     * 计算占比
     *
     * @param total 分母
     * @param cur   分子
     * @return 百分比
     */
    private BigDecimal calRadio(int total, int cur) {
        BigDecimal size = new BigDecimal(total);
        //计算插入的数据和总数据的占比
        return new BigDecimal(cur).divide(size, 2, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100));
    }

    /**
     * 取消导入
     *
     * @param model
     */
    public void cancel(@NotBlank(message = "model不能为空") String model) {
        String cancelKey = getCancelRedisKey(model);
        redisTemplate.boundValueOps(cancelKey).set(true);
    }

    /**
     * 清除excel读取的
     *
     * @param model
     */
    private void clearModelRedisData(String model) {
        String dataKey = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_ALL);
        if (redisTemplate.hasKey(dataKey)) {
            redisTemplate.delete(dataKey);
        }
        String checkPassKey = getRedisKey(model, RedisConstants.EXCEL_DATAMAP_SUCCESS);
        if (redisTemplate.hasKey(checkPassKey)) {
            redisTemplate.delete(checkPassKey);
        }
    }

    /**
     * 下载模板
     *
     * @param model
     * @return: java.lang.String
     * @author zhangxiaowei
     **/
    @SneakyThrows
    public void downloadTemplate(@NotBlank(message = "model不能为空") String model, HttpServletResponse response, ExcelEnum excelEnum) {
        if (Objects.isNull(excelEnum)) {
            excelEnum = ExcelEnum.XLSX;
        }
        String templateName = JsonParseUtil.getTemplateName(model);
        if (StringUtils.isBlank(templateName)) {
            throw new RuntimeException("模板json文件excel模板文件未定义");
        }
        String templatePath = excelConfigProperties.getTemplatePath() + File.separator + templateName;
        InputStream bis = new ClassPathResource(templatePath).getInputStream();
        templateName = URLEncoder.encode(templateName, "UTF-8");
        response.setContentType(excelEnum.getContentType());
        response.setCharacterEncoding("utf8");
        response.setHeader("Content-Disposition", "attachment;filename=" + templateName);
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
        int len = 0;
        while ((len = bis.read()) != -1) {
            out.write(len);
            out.flush();
        }
        out.close();
        bis.close();
    }

}
